
const fs = require('fs')
const Fn = require('./fn.js') // 基础功能函数
module.exports = {
  async upTable(cols, opt = { update: false }) {
    Adebug.log(this.ns, cols, opt)
    Object.assign(this.opt, opt)
    let unitLen = 0
    for (const k in cols) {
      const col = cols[k]
      if (!col.len) col.len = { bool: 1, float: 4, double: 8, time: 4 }[col.type]
      col.len += 1 // 多一个字节用于 $eq $ne
      if (this.cols[k]) { // 更新栏目
      } else if(!this.cols[k]) { // 新增栏目
        Adebug.log('add col', k, col)
      }
      unitLen += col.len
      this.cols[k] = col
    }
    this.opt.hotLimit = parseInt(opt.tableMem / unitLen * 1024 * 1.024) * 1000
    this.memSize = 0
    for (const k in this.cols) {
      const col = this.cols[k]
      if (!cols[k]) { // 删除列
        delete this.cols[k]
        delete this._buf[k]
        delete this._sab[k]
        Adebug.log('remove col', k, col)
      } else {
        await Fn.snapShotLoad(this, k, col, this.opt) // 加载老的列数据
        this.memSize += this._buf[k].byteLength
      }
    }
    this.saveTableInfo()
    return { ns: this.ns, cols: this.cols, opt: this.opt, inc: this.inc, hourList: this.hourList, oldInc: this.oldInc }
  },
  saveTableInfo() {
    const dbPath = [conf.db, this.db, this.name].join('/')
    if (!fs.existsSync(dbPath)) Acan.mkdirs(dbPath)
    fs.writeFileSync(dbPath + '/conf.json', JSON.stringify({ ns: this.ns, cols: this.cols, opt: this.opt }), { encoding: 'utf-8' })
  },
  create(data) { // 写入数据
    if (this.inc + 1 >= this.opt.hotLimit) return Fn.bufOverToFile(this) // console.log('over membuf')
    if (this.createLock) return Adebug.log('create lock')
    for (const k in this.cols) {
      const col = this.cols[k]
      if (!this._buf[k]) return
      if (!data[k]) data[k] = ''
      if (this.inc * col.len + col.len > this._buf[k].length || this.inc + 1 >= this.opt.hotLimit) { // 热数据超出大小
        // Fn.log('热数据已满，不能写入', this.inc, this.inc * col.len + col.len , this._buf[k].length)
        return
      }
      const offset = this.inc * col.len + 1
      const val = data[k] || col.default
      if (k === '_id') data[k] = Fn.typesWrite[col.type](this._buf[k], offset, col, val).toString('hex')
      else Fn.typesWrite[col.type](this._buf[k], offset, col, val)
    }
    this.inc++
    // 记录分钟的结束位置
    const hi = Acan.time('Hi')
    if (!this.incs[hi]) this.incs[hi] = 0
    this.incs[hi]++
    if (this.inc % 1000 === 0) Fn.bufOverToFile(this)
    return { _id: data._id, inc: this.inc }
  },
  creates(arr) { // 批量写入数据
    const _t = Fn._t()
    for (const o of arr) { this.create(o) }
    return { count: arr.length, hs: Fn._t() - _t }
  },
  _findOneIndex(con, offset = 0, date = '', opt = {}) { // 交替查找索引
    const { reverse = false } = opt
    const reStr = reverse ? 'lastIndexOf' : 'indexOf'
    const fi = {}
    let max = reverse ? (date ? this.fd.inc : this.inc) : -1
    let maxk = ''
    let isFalse = false // 单列是否匹配不成功
    let key = ''
    for (const k in con) {
      if (isFalse) break
      const col = this.cols[k]
      // 不存在的列 或为空 忽略条件
      if (!col || Acan.isEmpty(con[k])) { delete con[k]; continue }
      let i = -1
      if (date) {
        i = Fn.fdIndexOf(this.fd[date + '/' + k], con[k], col, offset * col.len, reStr)
      } else {
        i = Fn.bufIndexOne(this._buf[k].slice(0, this.inc * col.len + 1), con[k], col, offset * col.len, reStr)
      }
      // 匹配不到数据了
      if (i === -1) { isFalse = true, key = k; break }
      fi[i] = 1
      if (!reverse && i > max) max = i, maxk = k
      if (reverse && i < max) max = i, maxk = k
    }
    const ci = Object.keys(fi).length
    if (isFalse || ci === 0) return { max: -1, key } // 找不到数据 console.log('_findOneIndex 找不到数据', max, key),
    if (!date && max >= this.inc) return { max } // 找完数据 console.log('_findOneIndex 找完数据'),
    if (date && max >= this.fd.inc) return { max } // 找完数据 console.log('_findOneIndex 找完数据'),
    if (ci > 1) return this._findOneIndex(con, max, date, opt) // 不是同一行 往下找
    else return { max }
  },
  _findOne(con = {}, opt = {}) {
    let { offset = 0, select = null, date } = opt // offset 真实buf索引位置
    let index = { max: 0 }// 索引值 无条件取第一条
    if (!Acan.isEmpty(con)) { // 条件过滤
      index = this._findOneIndex(con, offset, date, opt)
      offset = index.max
    }
    // 超出内存查询范围
    if (offset >= this.inc && !date) {
      return { code: 405, message: 'offset over' }
    }
    return [offset, offset === -1 ? undefined : this.findRow(offset, select, date)]
  },
  async count(con, opt = {}) { // 统计
    const hash = Acan.SHA1(JSON.stringify({ con, date: opt.date }))
    // 指定时间段
    if (opt.date && opt.date !== Acan.time('Y-m-d/H')) {
      if (caches.data[hash] && opt.date) return caches.data[hash] // 历史数据查询进行缓存
      await Fn.loadFds(this, opt.date, opt.select)
    }
    const { date } = opt
    let count = 0
    // let arr = []
    let offset = 0
    const _t = Fn._t()
    const rs = {}
    const call = () => {
      // console.trace('count ok', con)
      rs.count = count
      rs.hs = Fn._tf(Fn._t() - _t)
      if (!date) rs.total = this.inc
      rs.offset = offset
      if (rs.total) rs.rate = (count * 100 / rs.total).toFixed(2) + '%'
      caches.add(hash, rs)
      return rs
    }
    if (date) {
      if (this.fd[date + '/_t']) rs.total = this.fd[date + '/_t'].size / 5
      if (this.fd[date + '/_id']) rs.total = this.fd[date + '/_id'].size / 13
    }
    // 无过滤条件
    if (Acan.isEmpty(con)) {
      if (!date) count = this.inc
      else count = rs.total
      return call()
    }
    const qcon = Fn.queryToBuf(con, this.cols)
    while (true) {
      if (date && offset >= this.fd.inc) return call() // 已经查完数据
      if (!date && offset >= this.inc) return call() // 已经查完数据
      index = this._findOneIndex(qcon, offset, opt.date)
      if (index.max === -1) return call()
      // arr.push(index.max)
      offset = index.max + 1
      count++
    }
  },
  _findLimit(con, opt = {}) {
    let { limit = 10, skip = 0, select = null, date, reverse = false } = opt
    const data = []
    let offset = skip
    if (reverse) {
      if (!date) offset = this.inc - 1
      else offset = this.fd.inc - 1
    }
    const call = (error) => {
      return Object.assign({ limit, data }, error)
    }
    const qcon = Fn.queryToBuf(con, this.cols)
    if (skip > 0 && !Acan.isEmpty(qcon)) { // 找出buf实际跳过的行位置 todo => caches
      for (let i = 0; i < skip; i++) {
        const index = this._findOneIndex(qcon, offset, date, opt)
        if (index.max === -1) return call()
        if (opt.reverse) offset = index.max - 1
        else offset = index.max + 1
      }
    } else {
      if (reverse) offset -= skip
      else offset += skip
    }
    for (let i = 0; i < limit; i++) {
      if (i === this.inc && !date) { break } // 超出已有的数据
      const d = this._findOne(qcon, { offset, select, date, reverse })
      if (d.code) return call(d) // 查询出错
      if (d[0] === -1 || !d[1]) return call() // 查询结束
      // if (offset >= this.inc) Adebug.log(limit, i, offset, d)
      if (reverse) offset = d[0] - 1
      else offset = d[0] + 1
      data.push(d[1])
    }
    return call()
  },
  async find(con, opt = {}) { // 读取数据
    if (opt.date) await Fn.loadFds(this, opt.date, opt.select) // 指定时间段
    const _t = Fn._t()
    const { limit = 10, skip = 0 } = opt
    if (opt.sort) opt.limit = this.inc // 存在排序
    const rs = Object.assign({ num: 0, hs: 0, skip }, this._findLimit(con, opt))
    if (opt.sort) {
      Fn._sort(rs.data, opt.sort)
      rs.data = rs.data.slice(0, limit)
    }
    rs.limit = limit
    rs.num = rs.data.length
    rs.hs = Fn._tf(Fn._t() - _t)
    return rs
  },
  async findz(con, opt = {}) { // 从zst压缩文件中读取数据
    if (opt.date) await Fn.loadZst(this, opt.date, opt.select) // 指定时间段
    const _t = Fn._t()
    const { limit = 10, skip = 0 } = opt
    if (opt.sort) opt.limit = this.inc // 存在排序
    const rs = Object.assign({ num: 0, hs: 0, skip }, this._findLimit(con, opt))
    if (opt.sort) {
      Fn._sort(rs.data, opt.sort)
      rs.data = rs.data.slice(0, limit)
    }
    rs.limit = limit
    rs.num = rs.data.length
    rs.hs = Fn._tf(Fn._t() - _t)
    return rs
  },
  async findOne(con = {}, opt = {}) {
    if (opt.date) await Fn.loadFds(this, opt.date) // 指定时间段
    const { skip = 0, select = null, date, reverse = false } = opt
    let offset = 0
    if (reverse) offset = this.inc - 1
    let index = { max: 0 }// 索引值 无条件取第一条
    if (!Acan.isEmpty(con)) { // 条件过滤
      const qcon = Fn.queryToBuf(con, this.cols)
      for (let i = 0; i < skip + 1; i++) { // 找出buf实际的行位置 todo => caches
        index = this._findOneIndex(qcon, offset, date, opt)
        if (i === skip || index.max === -1) { offset = index.max; break }
        offset = reverse ? index.max - 1 : index.max + 1
      }
    } else {
      if (reverse) offset -= skip
      else offset += skip
    }
    if (offset === -1) return { code: 405, param: index.key, message: `Can not found` } // 查询条件匹配不到数据
    if (!date && Math.abs(offset) > this.inc) return { code: 405, message: 'offset over' } // 超出范围
    if (reverse && offset < 0) return { code: 405, message: 'offset over' } // 超出范围
    return [offset, offset === -1 ? undefined : this.findRow(offset, select, date)]
  },
  findRow(i = 0, select = null, date = null) { // 读取指定行数据
    const rs = {}
    if (i > this.inc && !date) return undefined
    for (const k in this.cols) {
      if (select && !select.includes(k) && !select.includes('*') && !['_id', '_t'].includes(k)) continue
      const col = this.cols[k]
      if (!col) return Adebug.log(k, col)
      const start = i * col.len
      const key = date + '/' + k
      const _buf = date ? (this.fd[key] || {}).buf : this._buf[k]
      if (date && !_buf) { // 从磁盘中读取数据
        if (!this.fd[key]) continue // 无列数据
        const buf = Buffer.alloc(col.len - 1)
        fs.readSync(this.fd[key].fd, buf, 0, col.len - 1, start)
        rs[k] = Fn.types[col.type](buf, col.encoding)
      } else {
        if (!_buf) return undefined
        if (start + 1 >= _buf.length) return undefined // 已经读取完毕
        rs[k] = Fn.types[col.type](_buf.slice(start + 1, start + col.len), col.encoding)
      }
    }
    return rs
  },
  distinct(con, opt = {}) {
    const _t = Fn._t()
    const list = {}
    const col = this.cols[opt.distinct]
    if (!col) return { code: 404, message: `not field ${opt.distinct}` }
    for (let i = 0; i < this.inc; i++) {
      const start = 1 + i * col.len
      const val = Fn.types[col.type](this._buf[opt.distinct].slice(start, start + col.len - 1))
      list[val] = 1
    }
    const rs = { hs: Fn._t() - _t, count: 0, data: Object.keys(list) }
    delete list
    rs.count = rs.data.length
    return rs
  },
  _updataCol(index, k, val) {
    const col = this.cols[k]
    if (!col) return // 栏目不存在
    return Fn.typesWrite[col.type](this._buf[k], index * col.len + 1, col, val)
  },
  update(con, post) {
    const index = this._findOneIndex(Fn.queryToBuf(con, this.cols), 0)
    // Adebug.log(index)
    if (index.max === -1) return index
    for (const k in post) {
      if (['_id', '_t'].includes(k)) continue
      this._updataCol(index.max, k, post[k])
    }
    return index
  },
  tableInfo() {
    const rs = { ns: this.ns, db: this.db, name: this.name, inc: this.inc, memSize: this.memSize, overMoveLock: this.overMoveLock, opt: this.opt, cols: Acan.clone(this.cols) }
    Acan.each(rs.cols, (col) => {
      col.len--
    })
    return rs
  },
  async hourList(get) {
    const rs = { ns: this.ns, inc: 0, size: 0, disk: 0, zst: 0, data: [] }
    const nsPath = `${conf.db}/${this.db}/${this.name}/`
    let i = 0
    for (const day of fs.readdirSync(nsPath, { withFileTypes: true }).reverse()) {
      if (!day.isDirectory()) continue
      if (get.day && day.name !== get.day) continue
      if (i >= 2) break
      i++
      for (const hour of fs.readdirSync(nsPath + day.name, { withFileTypes: true }).reverse()) {
        if (!hour.isDirectory()) continue
        const hourPath = `${nsPath}${day.name}/${hour.name}/`
        let info = {}
        info = await Fn.countHourInfo(this.ns, !!this.cols._id, `${day.name}/${hour.name}`)
        rs.inc += info.inc
        rs.size += info.size
        rs.zst += info.zst
        rs.disk += info.zst || info.size
        rs.data.push(info)
      }
    }
    return rs
  }
}
